OceanBase事务引擎特性和应用实践分享
OB君:好消息!OceanBase现推出分布式数据库产品模块原理简介系列内容,通过完整13篇文章帮助数据库从业者建立更系统完善的数据库知识体系。第七期我们来聊聊分布式数据库中一个非常重要的技术门槛——事务。
Tips:关注OceanBase公众号回复“产品原理”获取OceanBase产品模块原理简介系列已发布的6篇文章合集(该系列持续更新中)。
很多人对数据库事务的ACID特性已经习以为常了,可能不会去细想其中的原理。传统关系数据库对ACID的实现原理也不完全相同,在分布式数据库下这个又有新的特点。
原子性(Atomicity)
在多线程开发中,原子性操作意味着其他线程无法看到这个操作的中间结果,只能看到其开始前和结束后的状态,这个在数据库中是通过隔离性(Isolation)体现。数据库事务的原子性指一个事务要么全部成功要么全部失败。这个挑战在于发生故障的时候。比如说进程崩溃,网络连接中断、磁盘变满或者写入异常等。此外,OceanBase是分布式数据库,业务的事务可能会修改多个节点的数据。OceanBase使用两阶段提交协议(2PC)来保证事务的原子性,确保相关多台机器上的事务要么都提交成功,要么都回滚。
隔离性(Isolation)
隔离性描述的是并发的事务读写相同的数据(即冲突)时的特点。不同的做法会有不同的结果或问题。比如说脏读问题、不可重复读、幻读、丢失更新、写偏序问题等。前面三个业务研发都很好理解,后面两个就很少见。针对这些问题数据库通过不同的隔离级别来应对。最严格的隔离级别是可序列化(Serializable),可序列化是指多个并发执行的事务结果等于这些事务按照某种顺序串行执行的结果。常用的隔离级别是读已提交(Read Committed)和可重复读(Read Repeatable)。OceanBase 1.0版本支持读已提交隔离级别,2.2版本支持可序列化隔离级别。
持久性是一个承诺,一旦事务提交成功,即使发生硬件故障或数据库崩溃,事务修改的任何数据都不会丢失(或能找回来)。关系型数据库在实现这个时候并不是把数据修改落到存储上,而是在每笔修改之前先记录相应的事务日志,然后把事务日志写到可靠的存储上。而数据却依然在数据库缓存里并不立即落盘。Oracle就是这样做的,数据主要是异步定时落盘(也有其他触发机制略去不提)。OceanBase的机制更特别,事务日志是在COMMIT的时候才生成并落盘,数据修改会一直不落盘,每天只落盘一次(后来增加转储机制,在增量内存不足的情况下可以转储到磁盘以释放内存)。因为有事务日志的保护以及三副本的高可用,OceanBase并不担心异常情形下数据丢失以及恢复速度过慢等问题。OceanBase跟传统关系型数据库最大的不同还是在于保证事务日志的可靠性方法上使用了Paxos协议(具体是Multiple-Paxos)。
OceanBase的读写跟传统数据库有很大的一点不同就是OceanBase的写并不是直接在数据块上修改,而是新开辟一块增量内存用于存放数据的变化。同一笔记录多次变化后增量块会以链表形式组织在一起,这些增量修改会一直在内存里不落盘。OceanBase读则是要把最早读入内存的数据块加上后续相关的增量块内容合并读出。这个读写分离的设计决定了OceanBase的事务特点。
OceanBase支持读已提交和可序列化两种隔离级别。在了解其原理之前先看事务实现的一些基本概念。
事务版本号
在数据库中,需要准确区分不同查询和修改的先后顺序。通常可能会认为根据时间判断即可。不过这个并不可靠,即使在单机数据库里,数据库时间也可能随着主机时间跳变。在分布式数据库下,不同节点的时间也不是完全严格的一致。即使是一致的,加上节点间网络通信的时间,依然无法判断不同修改的先后顺序。所以OceanBase实现了一个内部单调递增的时间戳。它跟时间有点关系,但不是时间。然后多个场景下会取这个时间戳。这个时间戳类似于Oracle的SCN。
读快照版本号(Snapshot Version):事务在修改数据的时候会先读取数据,需要决定读取哪个版本的数据,这个版本就是读快照版本。此后OceanBase只会读所有提交版本小于或等于读快照版本的最新已提交版本的数据,并且在读之后的所有事务提交时的提交版本都会大于这个读快照版本。这个机制保证了单个SQL没有脏读、不可重复读、幻读问题。
协调者流程:
Px收到end_trans消息后创建协调者状态机。协调者进入PREPARE状态并向所有参与者发送prepare消息,等候答复。
(a)如果收到所有参与者的prepare ok消息则进入COMMIT状态并向所有参与者发送commit消息。
(b)如果收到一个参与者abort ok消息则进入ABORT状态并向所有参与者发送abort消息。
参与者流程:
参与者状态机在DML语句执行过程中就创建了。在收到prepare消息后将事务的修改写日志并发起持久化操作。此时需要等三副本多数派持久化事务日志成功。
参与者事务日志持久化成功后进入PREPARED状态并回复协调者prepare ok消息;如果持久化失败则进入ABORTED状态并回复协调者abort ok消息。
(a)参与者收到协调者的commit消息则写commit日志,进入COMMITTED状态并回复commit ok消息。这个过程里会确定分布式事务的提交版本号并更新Public Version,释放行锁等。
(b)参与者收到协调者的abort消息则写abort日志,进入ABORTED状态并回复abort ok消息。
传统的两阶段提交的弊端在于对参与者的锁粒度太大,会阻塞相应数据的读。此外就是协调者宕机时分布式事务流程会进入不确定状态,应用提交会一直等待。可能有人会担心OB协调者宕机怎么办?
协调者宕机
协调者本身就是三副本,具有高可用能力。协调者宕机后在15s左右就可以自行恢复。参与者没有收到协调者信息时,参与者会定时重发上一条消息。协调者恢复后虽然没有协调者日志可以读取,但是可以通过参与者回复给协调者的消息里恢复出协调者当前处于的状态。
全局时间戳服务
外部一致性
全局时间戳服务
OceanBase 2.0开始实现了全局时间戳服务(Global Timestamp Service,简称GTS)。每个租户一个GTS服务,服务的架构采用C/S结构。租户的每个节点都会有个GTS Client,服务于节点内部的请求。GTS Server只有一个,依托于表__all_dummy表的Leader副本。同时GTS Server也就有高可用能力了。GTS用于获取一个租户内全局唯一且单调递增的版本号。使用全局时间戳服务获取一致性快照读版本这个又简称为全局一致性快照读。
当多个事务不是先后顺序关系时,则是并发事务。并发读写相同数据可能会存在一些冲突。
写写并发控制
OceanBase里这个锁等待不会无限制等待下去,每个SQL执行有个超时机制,由变量ob_query_timeout控制,默认10s。时间到时,DML SQL会报lock wait timeout。这个报错信息是取自MySQL,在MySQL里变量innodb_lock_wait_timeout会控制锁等待超时时间。详情参见《从ORACLE/MySQL到OceanBase:数据库超时机制》。
读写并发控制
select /*+ read_consistency(weak) */ * from t1 ...;
或
set session ob_read_consistency=WEAK; select * from t1 ...;
OceanBase复制表方案
OceanBase使用“复制表”的可能异常是如果“复制副本”出现不可用,会导致主副本COMMIT出现等待引起写性能下降。不过“复制副本”如果不可用了会很快被自动拉黑下次就不考虑这个“复制副本”而是依然访问主副本去。这个对业务来说是很好的。同样的,有拉黑逻辑就有赎回逻辑。如果“复制副本”恢复可用了很快就会继续服务。有关复制表使用案例以后有机会再详细介绍。
由于OceanBase的事务日志是在COMMIT的时候才生成并落盘,如果事务很大,会影响COMMIT的性能,同时还占用一定的内存资源。这个事务的大小多大合适并没有严格的说法。这取决于事务的并发数、内存的大小等等。通常建议并发很高的时候,事务大小(影响的记录数)控制在1000以内,并发低的话,放宽到10000笔也可以。这些只是建议,并不是代码逻辑。所以要根据实际情况反复测试再定。用一个事务更新几十万几百万记录的做法在OceanBase里也不建议的,应用写法虽然简单但把成本和风险都转移到数据库不是一个好的设计。
事务异常处理建议
如果数据库事务提交成功了,在答复客户端时网络发生故障,客户端会认为事务提交失败。简单重试事务会导致事务被执行两次。如果数据表上有唯一性约束会避免产生业务数据异常。 如果数据库错误是由于节点负载过大(性能瓶颈)导致的,重试事务会使得性能问题更加恶化。此时更好的做法是限制重试次数,单独处理与过载相关的错误。 如果是临时性错误(如死锁、网络抖动、故障切换),重试事务是建议的。如果是永久性错误(如违反约束),重试是无意义的。 如果业务事务还有非数据库操作(如发送电子邮件、写文件等),重试事务还会带来额外的副作用。当然在事务里就不应该包含非数据库操作。如果非要如此,则业务自己得实现两阶段提交机制。
以上异常处理分析适用于所有关系数据库,也包括OceanBase。实际测试也发现,OceanBase在节点负载非常高时(CPU利用率接近100%),分布式事务协调者和参与者之间的消息通信(RPC)可能会出现等待超时等情况,可能导致客户端收到的是一个事务状态未知的报错。尽管通过调整RPC相关参数可以避免报错,但不是根本的解决问题方法。OceanBase可以在线弹性扩容,租户性能瓶颈这个情况应当提前规划好资源,在需要的时候可以立即对租户或集群资源在线扩容。
事务错误码
-6001:ERROR 6001 (25000): OB-6001:Transaction set changed during the execution:事务在执行过程中SET改变。事务回滚。 -6002: ERROR 6002 (40000): OB-6002:transaction is rolled back:事务被回滚。 -6003:ERROR 1205 (HY000): OB-1205:Lock wait timeout exceeded; try restarting transaction:锁等待时间已经超过超时时间,尝试重新启动事务。当前SQL失败,不改变事务状态,需要应用重试或做其他处理。 -6004: ERROR 6004 (HY000): OB-6004:Shared lock conflict:执行INSERT/UPDATE/DELETE时,行锁冲突。 -6005:ERROR 6005 (HY000): OB-6005: Trylock row conflict:获取行锁冲突。 -6210:ERROR (25000): OB-4012:Transaction is timeout:事务超时。需要明确发起回滚才可以继续复用当前连接。 -6211:ERROR 6211 (25000): OB-6002:Transaction is killed:事务被杀。数据库端事务会回滚。 -6213:ERROR 6002 (HY000): OB-6002:Transaction context does not exist:事务内容不存在。数据库端事务会回滚。 -6224: ERROR 6002 (25000): OB-6002:transaction need rollback:事务需要回滚。不需要客户端发起回滚,数据库端事务会回滚。 -6225:ERROR 4012 (25000): OB-4012:Transaction result is unknown:事务结果未知。数据库端事务可能已提交或者回滚。需要业务重试。 -6226: ERROR 1792 (25006): OB-1792:Cannot execute statement in a READ ONLY transaction:不能在只读事务中执行语句。
参考文献:《设计数据密集型应用》
Tips:关注OceanBase公众号回复“产品原理”获取OceanBase产品模块原理简介系列已发布的6篇文章合集(该系列持续更新中)。
— 想了解更多OceanBase背后的技术秘密?
— 想与蚂蚁金服OceanBase的技术专家深入交流?
— 只需扫码关注OceanBase微信公众号并回复“加群”,快速加入OceanBase技术交流群!
— 有钉钉的小伙伴也可以加入OceanBase钉钉互动群:搜索群号21949783
● 最系统!一篇文章读懂OceanBase数据库的产品家族体系
● OceanBase SQL 引擎的模块介绍和调优实践分享
● OceanBase总控服务到底是啥?一文详解RootService总控服务
▼内容这么棒,还不赶紧扫码关注一下!▼